//chatServer

package chatroom;

import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static chatroom.CloseUtils.close;

public class chatServer {
    private CopyOnWriteArrayList<Channel> allUserChannel;//并发界面
    //CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候，不直接往当前容器添加，而是先将当前容器进行Copy，复制出一个新的容器，然后新的容器里添加元素，
    //添加完元素之后, 再将原容器的引用指向新的容器。
    //这样做的好处是我们可以对CopyOnWrite容器进行并发的读，而不需要加锁，因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想，读和写不同的容器。

    /*以下为窗口参数*/
    private JFrame frame;
    //头部参数
    private JTextField port_textfield;
    private JTextField name_textfield;
    private JButton head_connect;
    private JButton head_exit;
    private int port;//special
    //底部参数
    private JTextField text_field;
    private JTextField sysText_field;
    private JButton foot_send;
    private JButton foot_sysSend;
    private JButton foot_userClear;

    //右边参数
    private JLabel users_label;
    private JButton privateChat_button;
    private JButton kick_button;
    private JList<String> userlist;
    private DefaultListModel<String> users_model;

    //左边参数
    private JScrollPane sysTextScrollPane;
    private JTextPane sysMsgArea;
    private JScrollBar sysVertical;

    //中间参数
    private JScrollPane userTextScrollPane;
    private JTextPane userMsgArea;
    private JScrollBar userVertical;


    //时间格式化工具类
    static private SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");//设置时间

    //用户自增ID
    private int userId = 1000;
    //服务器管理员名字
    private String adminName;

    //服务器线程
    private ServerSocket serverSocket;

    //服务器线程
    private Server server;//special

    //管理员的 窗口队列 和 线程队列
    private HashMap<String, privateChatFrame> adminPrivateQueue;
    private HashMap<String, Channel> adminPrivateThread;


    public static void main(String[] args) {
        new chatServer().init();
    }


     //MethodName init   GUI初始化，初始化各种监听事件
    public void init() {
        allUserChannel = new CopyOnWriteArrayList<>();
        adminPrivateQueue = new HashMap<>();
        adminPrivateThread = new HashMap<>();

        setUIStyle();

        frame = new JFrame("Hcode聊天室服务器");
        JPanel panel = new JPanel();        /*主要的panel，上层放置连接区，下层放置消息区，
                                                  中间是消息面板，左边是room列表，右边是当前room的用户列表*/
        JPanel headpanel = new JPanel();    /*上层panel，用于放置连接区域相关的组件*/
        JPanel footpanel = new JPanel();    /*下层panel，用于放置发送信息区域的组件*/
        JPanel centerpanel = new JPanel();    /*中间panel，用于放置聊天信息*/
        JPanel leftpanel = new JPanel();    /*左边panel，用于放置房间列表和加入按钮*/
        JPanel rightpanel = new JPanel();   /*右边panel，用于放置房间内人的列表*/

        /*最上层的布局，分中间，东南西北五个部分*/
        BorderLayout layout = new BorderLayout();
        /*格子布局，主要用来设置西、东、南三个部分的布局*/
        GridBagLayout gridBagLayout = new GridBagLayout();
        /*主要设置北部的布局*/
        FlowLayout flowLayout = new FlowLayout();
        /*设置初始窗口的一些性质*/
        frame.setSize(900, 600);
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        frame.setContentPane(panel);
        frame.setLayout(layout);
        /*设置各个部分的panel的布局和大小*/
        headpanel.setLayout(flowLayout);
        footpanel.setLayout(gridBagLayout);
        leftpanel.setLayout(gridBagLayout);
        centerpanel.setLayout(gridBagLayout);
        rightpanel.setLayout(gridBagLayout);
        //设置面板大小
        leftpanel.setPreferredSize(new Dimension(350, 0));
        rightpanel.setPreferredSize(new Dimension(155, 0));
        footpanel.setPreferredSize(new Dimension(0, 40));

        //头部布局
        port_textfield = new JTextField("8888");
        name_textfield = new JTextField("匿名");
        port_textfield.setPreferredSize(new Dimension(70, 25));
        name_textfield.setPreferredSize(new Dimension(150, 25));

        JLabel port_label = new JLabel("端口号:");
        JLabel name_label = new JLabel("管理员:");

        head_connect = new JButton("启动");
        head_exit = new JButton("关闭");

        headpanel.add(port_label);
        headpanel.add(port_textfield);
        headpanel.add(name_label);
        headpanel.add(name_textfield);
        headpanel.add(head_connect);
        headpanel.add(head_exit);

        //底部布局
        foot_send = new JButton("发送聊天信息");
        foot_sysSend = new JButton("发送系统消息");
        foot_sysSend.setPreferredSize(new Dimension(110, 0));

        foot_userClear = new JButton("清空聊天消息");
        foot_userClear.setPreferredSize(new Dimension(148, 0));


        sysText_field = new JTextField();
        sysText_field.setPreferredSize(new Dimension(230, 0));
        text_field = new JTextField();
        footpanel.add(sysText_field, new GridBagConstraints(0, 0, 1, 1, 1, 1,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(1, 1, 1, 1), 0, 0));
        footpanel.add(foot_sysSend, new GridBagConstraints(1, 0, 1, 1, 1.0, 1.0,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(1, 1, 1, 3), 0, 0));
        footpanel.add(text_field, new GridBagConstraints(2, 0, 1, 1, 100, 100,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(1, 1, 1, 1), 0, 0));
        footpanel.add(foot_send, new GridBagConstraints(3, 0, 1, 1, 1.0, 1.0,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(1, 1, 1, 1), 0, 0));
        footpanel.add(foot_userClear, new GridBagConstraints(4, 0, 1, 1, 1.0, 1.0,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(1, 1, 1, 1), 0, 0));


        //左边布局
        JLabel sysMsg_label = new JLabel("系统日志：");
        sysMsgArea = new JTextPane();
        sysMsgArea.setEditable(false);
        sysTextScrollPane = new JScrollPane();
        sysTextScrollPane.setViewportView(sysMsgArea);
        sysVertical = new JScrollBar(JScrollBar.VERTICAL);
        sysVertical.setAutoscrolls(true);
        sysTextScrollPane.setVerticalScrollBar(sysVertical);
        leftpanel.add(sysMsg_label, new GridBagConstraints(0, 0, 1, 1, 1, 1,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
        leftpanel.add(sysTextScrollPane, new GridBagConstraints(0, 1, 1, 1, 100, 100,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));

        //右边布局

        users_label = new JLabel("当前连接用户：0");
        privateChat_button = new JButton("私聊");
        kick_button = new JButton("踢出");

        users_model = new DefaultListModel<>();//先默认设置
        userlist = new JList<String>(users_model);


        JScrollPane userListPane = new JScrollPane(userlist);
        //创建一个显示指定组件内容的 JScrollPane，只要组件的内容大于视图，就会出现水平和垂直滚动条

        rightpanel.add(users_label, new GridBagConstraints(0, 0, 1, 1, 1, 1,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
        rightpanel.add(privateChat_button, new GridBagConstraints(0, 1, 1, 1, 1, 1,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
        rightpanel.add(kick_button, new GridBagConstraints(0, 2, 1, 1, 1, 1,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
        rightpanel.add(userListPane, new GridBagConstraints(0, 3, 1, 1, 100, 100,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));

        //中间布局
        JLabel userMsg_label = new JLabel("世界聊天：");
        userMsgArea = new JTextPane();
        userMsgArea.setEditable(false);
        userTextScrollPane = new JScrollPane();
        userTextScrollPane.setViewportView(userMsgArea);
        userVertical = new JScrollBar(JScrollBar.VERTICAL);
        userVertical.setAutoscrolls(true);
        userTextScrollPane.setVerticalScrollBar(userVertical);

        centerpanel.add(userMsg_label, new GridBagConstraints(0, 0, 1, 1, 1, 1,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
        centerpanel.add(userTextScrollPane, new GridBagConstraints(0, 1, 1, 1, 100, 100,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));

        //设置顶层布局
        panel.add(headpanel, "North");
        panel.add(footpanel, "South");
        panel.add(leftpanel, "West");
        panel.add(rightpanel, "East");
        panel.add(centerpanel, "Center");



        //将按钮事件全部注册到监听器
        allActionListener allActionListener = new allActionListener();
        //开启服务
        head_connect.addActionListener(allActionListener);
        // 管理员发布消息
        foot_send.addActionListener(allActionListener);
        //关闭服务器
        head_exit.addActionListener(allActionListener);
        //清空消息日志
        foot_sysSend.addActionListener(allActionListener);
        //清空世界聊天消息
        foot_userClear.addActionListener(allActionListener);
        //私聊
        privateChat_button.addActionListener(allActionListener);
        //踢人
        kick_button.addActionListener(allActionListener);

        //服务器窗口关闭事件
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                int option = JOptionPane.showConfirmDialog(frame, "确定关闭服务器界面?", "提示",
                        JOptionPane.YES_NO_OPTION);
                if (option == JOptionPane.YES_OPTION) {
                    if (e.getWindow() == frame) {
                        if (server != null) { //如果已开启服务，就告诉各个已连接客户端：服务器已关闭
                            sendSysMsg("由于服务器关闭，已断开连接", 3);
                        }
                        frame.dispose();
                        System.exit(0);
                    } else {
                        return;
                    }
                } else {
                    return;
                }
            }
        });

        //聊天信息输入框的监听回车按钮事件
        text_field.addKeyListener(new KeyAdapter() {
            @Override
            public void keyTyped(KeyEvent e) {

                if (e.getKeyChar() == KeyEvent.VK_ENTER) {
                    if (server == null) {
                        JOptionPane.showMessageDialog(frame, "请先开启聊天室的服务器！", "提示", JOptionPane.WARNING_MESSAGE);
                        return;
                    }

                    String text = text_field.getText();//get,put,set
                    if (text != null && !text.equals("")) {//""和空不会有任何提示，只是没反应
                        sendAdminMsg(text);
                        text_field.setText("");//重置归零，重新为空
                        insertMessage(userTextScrollPane, userMsgArea, null, "[管理员]" + adminName +" "+df.format(new Date()) , " "+text, userVertical, false);
                    }
                }
            }
        });

        // 系统信息输入框的回车监控事件
        sysText_field.addKeyListener(new KeyAdapter() {
            @Override
            public void keyTyped(KeyEvent e) {

                if (e.getKeyChar() == KeyEvent.VK_ENTER) {
                    if (server == null) {
                        JOptionPane.showMessageDialog(frame, "请先开启聊天室的服务器！", "提示", JOptionPane.WARNING_MESSAGE);
                        return;
                    }
                    String sysText = sysText_field.getText(); //获取输入框中的内容
                    if (sysText != null && !sysText.equals("")) {
                        sendSysMsg(sysText, 2);
                        sysText_field.setText("");
                        insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统日志] " + df.format(new Date()), "[管理员]" + adminName + "：" + sysText, sysVertical, true);
                    }
                }
            }
        });

        //窗口显示
        frame.setVisible(true);

        String name = JOptionPane.showInputDialog("请输入本聊天室管理员昵称：");
        if (name != null &&!name.equals("")) {
            name_textfield.setText(name);//the new text to be set
            //将此 TextComponent 的文本设置为指定的文本。
            //Sets the text of this TextComponent to the specified text.
        }
    }



     //MethodName ipCheckPort
     //验证端口格式是否准确
    public static boolean ipCheckPort(String text){
        return text.matches("([0-9]|[1-9]\\d{1,3}|[1-5]\\d{4}|6[0-5]{2}[0-3][0-5])");
    }


     //按钮监听内部类
     //Function: 全局监听事件，监听所有按钮

    private class allActionListener implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            String cmd = e.getActionCommand();
            //返回的是事件源组件的“命令” ， 这个“命令” 实际上就是事件源组件上的“Label（标签）字符串”
            switch (cmd) {
                case "启动":
                    String strport = port_textfield.getText();

                    if (!ipCheckPort(strport)){
                        JOptionPane.showMessageDialog(frame, "请使用0~65535的整数作为端口号！", "失败", JOptionPane.ERROR_MESSAGE);
                        break;
                    }

                    port = Integer.parseInt(strport);
                    try {
                        server = new Server(new ServerSocket(port));
                        insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统日志] " + df.format(new Date()), "服务器开启成功！", sysVertical, true);
                        head_connect.setText("已启动");
                        head_exit.setText("关闭");
                        (new Thread(server)).start();//special
                        adminName = name_textfield.getText();
                        name_textfield.setEditable(false);
                        port_textfield.setEditable(false);
                    } catch (IOException ee) {
                        //端口被占用
                        insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统日志] " + df.format(new Date()), "错误：端口号被占用！", sysVertical, true);
                        JOptionPane.showMessageDialog(frame, "开启服务器失败！端口被占用，请更换端口号！", "失败", JOptionPane.ERROR_MESSAGE);
                    }
                    break;
                case "关闭":
                    if (server == null) {
                        JOptionPane.showMessageDialog(frame, "不能关闭，未开启过服务器！", "错误", JOptionPane.ERROR_MESSAGE);
                        break;
                    }
                    sendSysMsg("由于服务器关闭，已断开连接", 3);
                    try {
                        serverSocket.close();
                    } catch (Exception e1) {
                        insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统日志] " + df.format(new Date()), "错误：服务器关闭失败！", sysVertical, true);
                    }
                    head_connect.setText("启动");
                    head_exit.setText("已关闭");
                    port_textfield.setEditable(true);
                    name_textfield.setEditable(true);
                    for (Channel channel : allUserChannel) {
                        channel.release();
                    }
                    server = null;
                    users_model.removeAllElements();
                    insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统日志] " + df.format(new Date()), "服务器已关闭！", sysVertical, true);
                    JOptionPane.showMessageDialog(frame, "服务器已关闭！");
                    break;
                case "发送系统消息":
                    if (server == null) {
                        JOptionPane.showMessageDialog(frame, "请先开启聊天室服务器！", "提示", JOptionPane.WARNING_MESSAGE);
                        break;
                    }

                    String sysText = sysText_field.getText(); //获取输入框中的内容
                    sendSysMsg(sysText, 2);
                    sysText_field.setText("");
                    insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统日志] " + df.format(new Date()), "[管理员]" + adminName + "：" + sysText, sysVertical, true);
                    break;
                case "发送聊天信息":
                    if (server == null) {
                        JOptionPane.showMessageDialog(frame, "请先开启聊天室服务器！", "提示", JOptionPane.WARNING_MESSAGE);
                        break;
                    }

                    String text = text_field.getText(); //获取输入框中的内容
                    sendAdminMsg(text);
                    text_field.setText("");
                    insertMessage(userTextScrollPane, userMsgArea, null, "[管理员]" + adminName +" "+df.format(new Date()) , " "+text, userVertical, false);
                    break;
                case "踢出":
                    if (server == null) {
                        JOptionPane.showMessageDialog(frame, "请先开启聊天室服务器！", "提示", JOptionPane.WARNING_MESSAGE);
                        break;
                    }
                    String selected = null;//初始化字符串
                    try {
                        selected = userlist.getSelectedValue();//返回最小选定单元格索引的值； 仅在列表中选择单个项目时的选定值
                        kickUser(selected);
                    } catch (NullPointerException e1) {
                        JOptionPane.showMessageDialog(frame, "请点击选择需要踢出的用户", "错误", JOptionPane.ERROR_MESSAGE);
                    }
                    break;
                case "清空系统日志":
                    sysMsgArea.setText("");
                    break;
                case "清空聊天消息":
                    userMsgArea.setText("");
                    break;
                case "私聊":
                    if (server == null) {
                        JOptionPane.showMessageDialog(frame, "请先开启聊天室服务器！", "提示", JOptionPane.WARNING_MESSAGE);
                        break;
                    }
                    String privateSelected = userlist.getSelectedValue();
                    privateChat(privateSelected);
                    break;
                default:
                    break;
            }
        }
    }


     //MethodName insertMessage
     //往 系统消息文本域 或者 聊天事件文本域 插入固定格式的内容
    private void insertMessage(JScrollPane scrollPane, JTextPane textPane, String icon_code,
                               String title, String content, JScrollBar vertical, boolean isSys) {

        StyledDocument document = textPane.getStyledDocument();     /*获取textpane中的文本*/
        //通用样式文档的接口，Document是用于文本的容器，用作swing文本组件的模型
        //获取与编辑器关联的模型

        /*设置标题的属性*/
        Color content_color = null;//默认颜色
        if (isSys) {
            content_color = Color.RED;
        } else {
            content_color = Color.GRAY;
        }

        SimpleAttributeSet title_attr = new SimpleAttributeSet();
        StyleConstants.setBold(title_attr, true);
        StyleConstants.setForeground(title_attr, Color.BLUE);
        /*设置正文的属性*/
        SimpleAttributeSet content_attr = new SimpleAttributeSet();
        StyleConstants.setBold(content_attr, false);
        StyleConstants.setForeground(content_attr, content_color);

        Style style = null;//不同的元素：文字、图片

        if (icon_code != null) {
            Icon icon = new ImageIcon("icon/" + icon_code + ".png");
            //Icon——A small fixed size picture, typically used to decorate components.

            //从指定的文件创建一个图像图标。 使用 MediaTracker 监控图像的加载状态，将预加载图像。 指定的字符串可以是文件名或文件路径。
            //指定路径时，使用 Internet 标准的正斜杠 ("/") 作为分隔符。 （字符串被转换为 URL，因此正斜杠适用于所有系统。）
            //例如，指定：new ImageIcon("images/myImage.gif") 描述被初始化为文件名字符串。

            style = document.addStyle("icon", null);//更改格式
            StyleConstants.setIcon(style, icon);//实际应用
        }

        try {
            document.insertString(document.getLength(), title + "\n", title_attr);
            if (style != null)
                document.insertString(document.getLength(), "\n", style);
            else
                document.insertString(document.getLength(), content + "\n", content_attr);

        } catch (BadLocationException ex) {
            System.out.println("Bad location exception");
        }
        /*设置滑动条到最后*/
        textPane.setCaretPosition(textPane.getDocument().getLength());
        //设置 TextComponent 的文本插入符号的位置。 该位置必须介于 0 和组件文本的长度之间，否则将引发异常。
    }


     //MethodName sendSysMsg
     //发布系统消息
    private void sendSysMsg(String content, int code) {
        if (code == 2) {
            String msg = "<time>" + df.format(new Date()) + "</time><sysMsg>" + content + "</sysMsg>";

            for (Channel channel : allUserChannel) {
                channel.send(formatCodeWithMsg(msg, code));
            }

        } else if (code == 3) {
            for (Channel channel : allUserChannel) {
                channel.send(formatCodeWithMsg(content, code));
            }
        }
    }


     //MethodName sendAdminMsg
     //发送管理员聊天

    private void sendAdminMsg(String content) {
        String msg = "<userId>0</userId><userMsg>" + content + "</userMsg><time>" + df.format(new Date()) + "</time>";
        for (Channel channel : allUserChannel) {
            channel.send(formatCodeWithMsg(msg, 1));
        }
    }


     //MethodName kickUser
     //踢出操作,对选中用户进行踢出操作，顺便向其它用户说明
    private void kickUser(String selected) {

        String kickedUserName = null;
        String kickedUserId  = null;

        //管理员还在与之私聊不可踢！避免冲突！
        for (Channel channel1 : allUserChannel) {
            String tmp = "[用户" + channel1.user.getId() + "]" + channel1.user.getUsername();
            if (adminPrivateThread.containsKey(channel1.user.getId().toString())) {
                //containsKey——Returns true if this map contains a mapping（HashMap） for the specified key.
                JOptionPane.showMessageDialog(frame, "管理员与该用户的私聊未结束，无法踢出！", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            if(tmp.equals(selected)){
                kickedUserName = tmp;
                kickedUserId = channel1.user.getId().toString();
                //a string representation of the value of this object in base 10.
            }
        }
        String kickedUserMsg = "对不起，您已被本聊天室管理员踢出！";
        String otherUserMsg = "<time>" + df.format(new Date()) + "</time><sysMsg>" +"通知："+kickedUserName+" 被踢出了聊天室！" + "</sysMsg>";

        for (Channel channel2 : allUserChannel) {
            String tmp = "[用户" + channel2.user.getId() + "]" + channel2.user.getUsername();
            if (tmp.equals(selected)) {
                //告诉对方你被踢了
                channel2.send(formatCodeWithMsg(kickedUserMsg, 3));
                //服务器端系统记录
                insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统日志] " + df.format(new Date()) , tmp+" 被踢出了聊天室", sysVertical, true);
                //服务器端界面用户列表移除对应用户
                users_model.removeElement(selected);
                channel2.release();
                users_label.setText("当前连接用户：" + allUserChannel.size());
                break;
            }else{
                //通知每个用户 此人被踢出聊天室
                channel2.send(formatCodeWithMsg(otherUserMsg,2));
                channel2.send(formatCodeWithMsg(kickedUserId,5));
            }
        }
    }


     //MethodName privateChat
     //管理员私聊操作
    private void privateChat(String selected) {
        for (Channel channel : allUserChannel) {
            String tmp = "[用户" + channel.user.getId() + "]" + channel.user.getUsername();
            if (tmp.equals(selected)) {
                if(adminPrivateQueue.containsKey(channel.user.getId().toString())){ //不能重复私聊
                    JOptionPane.showMessageDialog(frame, "与该用户私聊窗口已存在，请不要重复私聊！", "错误", JOptionPane.ERROR_MESSAGE);
                    return;
                }

                String Msg = "<from>[管理员]" + adminName+ "</from><id>0</id>";
                channel.send(formatCodeWithMsg(Msg, 9)); //将自己的个人信息发给想要私聊的用户线程
                break;
            }
        }
    }

     //MethodName formatCodeWithMsg
     //消息与命令的格式化
    private String formatCodeWithMsg(String msg, int code) {
        return "<cmd>" + code + "</cmd><msg>" + msg + "</msg>\n";
    }

     //ClassName Server
     //服务器开启TCP接受客户端信息的线程，内部类实现
    private class Server implements Runnable {

        private Server(ServerSocket socket) {
            serverSocket = socket;
        }

        @Override
        public void run() {
            while (true) {
                Socket client = null;
                try {
                    client = serverSocket.accept();
                    //Listens for a connection to be made to this socket and accepts it.
                    //The method blocks until a connection is made.
                    //Socket socket=ss.accept();监听客户端：这里是阻塞的，为了避免阻塞，把asServer放在线程中

                    userId++;
                    User user = new User("user" + userId, userId, client);

                    Channel channel = new Channel(user);
                    allUserChannel.add(channel);
                    users_label.setText("当前连接用户：" + allUserChannel.size());
                    (new Thread(channel)).start();
                } catch (IOException e) {
                    //关闭不处理
                    break;
                }
            }
        }
    }


     //ClassName Channel
     //内部类 启动一个线程应对一个客户端的服务
    protected class Channel implements Runnable {
        private DataInputStream dis;
        private DataOutputStream dos;
        private boolean isRunning;
        private User user;
        private CopyOnWriteArrayList<Channel> shieldList;
        private HashMap<String, Channel> privateQueue;

        public Channel(User user) {
            try {
                this.dis = new DataInputStream(user.getSocket().getInputStream());
                this.dos = new DataOutputStream(user.getSocket().getOutputStream());
                this.shieldList = new CopyOnWriteArrayList<>();
                //Creates an empty list.
                //ArrayList 的线程安全变体，其中所有可变操作（添加、设置等）都是通过制作底层数组的新副本来实现的。
                this.privateQueue = new HashMap<>();
                this.isRunning = true;
                this.user = user;
            } catch (IOException var3) {
                System.out.println("聊天室服务器初始化失败");
                this.release();
            }

        }

        public User getUser() {
            return user;
        }

        public CopyOnWriteArrayList<Channel> getShieldList() {
            return shieldList;
        }
        //CopyOnWriteArrayList是在有写操作的时候会copy一份数据，然后写完再设置成新的数据

        private void parseMsg(String msg) {//对应sendMsg中的两块进行拆解和处理
            String code = null;
            String message = null;

            if (msg.length() > 0) {

                //获取指令码
                Pattern pattern = Pattern.compile("<cmd>(.*)</cmd>");
                Matcher matcher = pattern.matcher(msg);
                if (matcher.find()) {
                    code = matcher.group(1);
                }

                //获取消息
                pattern = Pattern.compile("<msg>(.*)</msg>");
                matcher = pattern.matcher(msg);
                if (matcher.find()) {
                    message = matcher.group(1);
                }

                switch (code) {
                    // 该服务器线程对应的客户端线程新用户刚加入
                    case "new":
                        user.setUsername(message);//加工信息
                        if (!users_model.contains("[用户" + user.getId() + "]" + user.getUsername())) {
                            //Tests whether the specified object is a component in this list
                            users_model.addElement("[用户" + user.getId() + "]" + user.getUsername());
                        }

                        String title = "[系统日志] " + df.format(new Date());
                        String content = "[用户" + user.getId() + "]" + user.getUsername() + "  加入了聊天室";
                        insertMessage(sysTextScrollPane, sysMsgArea, null, title, content, sysVertical, true);

                        sendAnyone(formatCodeWithMsg("<username>" + user.getUsername() + "</username><id>" + user.getId() + "</id>", 4), false);
                        //给当前线程服务的客户端id
                        send(formatCodeWithMsg(String.valueOf(user.getId()), 8));
                        break;
                    case "exit":
                        if (users_model.contains("[用户" + user.getId() + "]" + user.getUsername())) {
                            users_model.removeElement("[用户" + user.getId() + "]" + user.getUsername());
                        }

                        String logTitle = "[系统日志] " + df.format(new Date());
                        String logContent = "[用户" + user.getId() + "]" + user.getUsername() + "  退出了聊天室";
                        insertMessage(sysTextScrollPane, sysMsgArea, null, logTitle, logContent, sysVertical, true);

                        allUserChannel.remove(this);
                        //Removes the first occurrence of the specified element from this list, if it is present.

                        sendAnyone(formatCodeWithMsg("" + user.getId(), 5), false);
                        this.release(); //移除用户，关闭线程。
                        break;
                    case "getList":
                        //给客户端传送
                        send(formatCodeWithMsg(getUsersList(), 7));
                        break;
                    case "msg":
                        String now = df.format(new Date());
                        //写入服务端的聊天世界中
                        insertMessage(userTextScrollPane, userMsgArea, null,  "[用户" + user.getId() + "]" + user.getUsername() + " "+now, " "+message, userVertical, false);
                        // 将自己说的话发给每个人
                        sendAnyone(formatCodeWithMsg("<userId>" + user.getId() + "</userId><userMsg>" + message + "</userMsg><time>" + now + "</time>", 1), false);
                        break;
                    case "buildPrivateChat":
                        //建立私聊机制

                        //如果私聊对象是管理员
                        if (message.equals("0")){
                            int option = JOptionPane.showConfirmDialog(frame, "[" + user.getUsername() + "]想与你私聊，是否同意？", "提示",
                                    JOptionPane.YES_NO_OPTION);
                            if (option == JOptionPane.YES_OPTION) { //同意私聊
                                String agreeMsg = "<result>1</result><from>" + adminName+ "</from><id>0</id>";
                                send(formatCodeWithMsg(agreeMsg, 10));
                                privateChatFrame privateChatFrame = new privateChatFrame("与[" + user.getUsername() + "]的私聊窗口", user.getUsername(), user.getId().toString());
                                adminPrivateQueue.put(user.getId().toString(),privateChatFrame);
                                adminPrivateThread.put(user.getId().toString(), this);
                            }else{ //拒绝私聊
                                String refuseMsg = "<result>0</result><from>" + adminName + "</from><id>0</id>";
                                send(formatCodeWithMsg(refuseMsg, 10));
                            }
                        }else { //普通用户私聊对象
                            for (Channel channel : allUserChannel) {
                                if (channel.getUser().getId() == Integer.parseInt(message)) {
                                    String Msg = "<from>" + user.getUsername() + "</from><id>" + user.getId() + "</id>";
                                    this.privateQueue.put(message, channel); //先将对方放入私聊队列
                                    channel.privateQueue.put(user.getId().toString(), this); //对方也将你放入私聊队列
                                    channel.send(formatCodeWithMsg(Msg, 9)); //将自己的个人信息发给想要私聊的用户线程
                                    break;
                                }
                            }
                        }
                        break;
                    case "agreePrivateChar": //同意与此ID的人进行私聊
                        if (message.equals("0")){ //如果对方是管理员
                            privateChatFrame privateChatFrame = new privateChatFrame("与[" + user.getUsername() + "]的私聊窗口", user.getUsername(), user.getId().toString());
                            adminPrivateQueue.put(user.getId().toString(),privateChatFrame);
                            adminPrivateThread.put(user.getId().toString(), this);
                        }else { //普通用户
                            String agreeMsg = "<result>1</result><from>" + user.getUsername() + "</from><id>" + user.getId() + "</id>";
                            privateQueue.get(message).send(formatCodeWithMsg(agreeMsg, 10));
                        }
                        break;
                    case "refusePrivateChar"://拒绝与此ID的人进行私聊，从私聊队列移除
                        if(message.equals("0")){ //如果是管理员
                            JOptionPane.showMessageDialog(frame, "[" + user.getUsername() + "]拒绝了你的私聊请求", "失败", JOptionPane.ERROR_MESSAGE);
                        }else {
                            String refuseMsg = "<result>0</result><from>" + user.getUsername() + "</from><id>" + user.getId() + "</id>";
                            privateQueue.get(message).send(formatCodeWithMsg(refuseMsg, 10));
                            privateQueue.get(message).privateQueue.remove(user.getId()); //对方也移除
                            privateQueue.remove(message); //移除对方
                        }
                        break;
                    case "privateMsg": //转发私聊消息
                        Pattern privatePattern = Pattern.compile("<msg>(.*)</msg><id>(.*)</id>");
                        Matcher privateMatcher = privatePattern.matcher(message);
                        if (privateMatcher.find()) {
                            String toPrivateMsg = privateMatcher.group(1);
                            String toPrivateId = privateMatcher.group(2);

                            if (toPrivateId.equals("0")){ //想要发给管理员
                                //管理员主线程获取当前服务线程对应的私聊窗口
                                privateChatFrame nowPrivateChat = adminPrivateQueue.get(user.getId().toString());
                                insertMessage(nowPrivateChat.textScrollPane,nowPrivateChat.msgArea,null, df.format(new Date()) + " 对方说：", " "+toPrivateMsg, nowPrivateChat.vertical, false);
                            }else {
                                String resultMsg = "<msg>" + toPrivateMsg + "</msg><id>" + user.getId() + "</id>";
                                //根据信息来源ID（想要转发的用户ID）找到对应线程将自己的id和信息发给他
                                privateQueue.get(toPrivateId).send(formatCodeWithMsg(resultMsg, 11));
                            }
                        }
                        break;
                    case "privateExit":
                        if (message.equals("0")){ //如果是管理员
                            JOptionPane.showMessageDialog(frame, "由于对方结束了私聊，该私聊窗口即将关闭！", "提示", JOptionPane.WARNING_MESSAGE);
                            adminPrivateQueue.get(user.getId().toString()).dispose();
                            insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统消息] " + df.format(new Date()), "由于[" + user.getUsername() + "]关闭了私聊窗口，私聊结束！", sysVertical, true);
                            adminPrivateQueue.remove(user.getId().toString());   //移除此私聊对话窗口
                            adminPrivateThread.remove(user.getId().toString());  //移除此私聊对话窗口线程
                            //Removes the mapping for the specified key from this map if present
                            //如果存在，则从此映射中删除指定键的映射
                        }else {//普通用户私聊
                            String endMsg = "<id>" + user.getId() + "</id>";
                            privateQueue.get(message).send(formatCodeWithMsg(endMsg, 12));
                            privateQueue.get(message).privateQueue.remove(this.user.getId().toString()); //先自己，一会对方也移除
                            privateQueue.remove(message);//移除对方
                        }
                        break;
                    case "shield": //将传来的id对应的服务线程加入到屏蔽列表里面
                        for (Channel channel : allUserChannel) {
                            if (channel.getUser().getId() == Integer.parseInt(message)) {
                                if (!shieldList.contains(channel)) {
                                    shieldList.add(channel);
                                }
                                break;
                            }
                        }
                       // System.out.println("屏蔽时："+shieldList);
                        break;
                    case "unshield": //将传来的id对应的服务线程从屏蔽列表里面删除
                        for (Channel channel : allUserChannel) {
                            if (channel.getUser().getId() == Integer.parseInt(message)) {
                                if (shieldList.contains(channel)) {
                                    shieldList.remove(channel);
                                }
                                break;
                            }
                        }
                       // System.out.println("取消屏蔽时："+shieldList);
                        break;
                    case "setName":
                        //改了昵称，跟其它客户端的用户列表进行更正
                        users_model.removeElement(user.getId() + "#@" + user.getUsername());
                        user.setUsername(message);
                        users_model.addElement(user.getId() + "#@" + user.getUsername());
                        sendAnyone(formatCodeWithMsg("<id>" + user.getId() + "</id><username>" + message + "</username>", 6), false);
                        break;
                    default:
                        System.out.println("not valid message from user" + user.getId());
                        break;
                }
            }
        }


        private String getUsersList() {
            StringBuffer stringBuffer = new StringBuffer();
            /*获得房间中所有的用户的列表，然后构造成一定的格式发送回去*/
            stringBuffer.append("<user><id>0</id><username>" + adminName + "</username></user>");
            for (Channel each : allUserChannel) {
                stringBuffer.append("<user><id>" + each.getUser().getId() +
                        "</id><username>" + each.getUser().getUsername() + "</username></user>");
            }
            return stringBuffer.toString();
        }

        private String receive() {
            String msg = "";
            try {
                msg = this.dis.readUTF();
            } catch (IOException var3) {
                this.release();
            }

            return msg;
        }

        public void send(String msg) {
            try {
                this.dos.writeUTF(msg);
                this.dos.flush();
            } catch (IOException var3) {
                insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统日志] "+df.format(new Date()), "用户[" + user.getUsername() + "]的服务器端服务线程出错，请重启服务器！", sysVertical, true);
                this.release();
            }

        }

        private void sendAnyone(String msg, boolean isSys) {

            for (Channel userChannel : allUserChannel) {
                //获取每个用户的线程类
                if (!userChannel.getShieldList().contains(this)) {//当前服务线程不在对方线程屏蔽组内的才发送信息
                    userChannel.send(msg);
                }
            }
        }

        //释放资源
        public void release() {
            this.isRunning = false;
            close(dis, dos, this.user.getSocket());
            // 列表中移除用户
            users_model.removeElement(user.getId() + "#@" + user.getUsername());
            if (allUserChannel.contains(this)) {
                allUserChannel.remove(this);
            }
            users_label.setText("当前连接用户：" + allUserChannel.size());
        }

        @Override
        public void run() {
            while (isRunning) {
                String msg = receive();
                if (!msg.equals("")) {
                    parseMsg(msg);
                }
            }
        }
    }

    /**
     * @ClassName privateChatFrame
     * @Params  * @param null
     * @Description 管理员所属的私聊窗口内部类
     * @Return
     * @Since 2020/6/6
     */
    private class privateChatFrame extends JFrame {
        private String otherName;
        private String otherId;
        private JButton sendButton;
        private JTextField msgTestField;
        private JTextPane msgArea;
        private JScrollPane textScrollPane;
        private JScrollBar vertical;

        public privateChatFrame(String title, String otherName, String otherId) throws HeadlessException {
            super(title);
            this.otherName = otherName;
            this.otherId = otherId;
            //全局面板容器
            JPanel panel = new JPanel();
            //全局布局
            BorderLayout layout = new BorderLayout();

            JPanel headpanel = new JPanel();    //上层panel，
            JPanel footpanel = new JPanel();    //下层panel
            JPanel centerpanel = new JPanel(); //中间panel

            //头部布局
            FlowLayout flowLayout = new FlowLayout();
            //底部布局
            GridBagLayout gridBagLayout = new GridBagLayout();

            setSize(600, 500);
            setLocationRelativeTo(null);
            setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
            //窗口关闭事件
            addWindowListener(new WindowAdapter() {
                @Override
                public void windowClosing(WindowEvent e) {
                    int option = JOptionPane.showConfirmDialog(e.getOppositeWindow(), "确定结束私聊？", "提示",
                            JOptionPane.YES_NO_OPTION);
                    if (option == JOptionPane.YES_OPTION) {
                        if (server != null) {
                            //关闭当前私聊连接
                            String endMsg = "<id>0</id>";
                            adminPrivateThread.get(otherId).send(formatCodeWithMsg(endMsg, 12)); //关闭当前私聊连接
                            adminPrivateQueue.remove(otherId);   //移除此私聊对话窗口
                            adminPrivateThread.remove(otherId);
                        }
                        insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统消息] " + df.format(new Date()), "您与[" + otherName + "]的私聊结束", sysVertical, true);
                        dispose();
                    } else {
                        return;
                    }
                }
            });
            setContentPane(panel);
            setLayout(layout);

            headpanel.setLayout(flowLayout);
            footpanel.setLayout(gridBagLayout);
            footpanel.setPreferredSize(new Dimension(0, 40));
            centerpanel.setLayout(gridBagLayout);

            //添加头部部件
            JLabel Name = new JLabel(otherName);
            headpanel.add(Name);

            //设置底部布局
            sendButton = new JButton("发送");
            sendButton.setPreferredSize(new Dimension(40, 0));
            msgTestField = new JTextField();
            footpanel.add(msgTestField, new GridBagConstraints(0, 0, 1, 1, 100, 100,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(1, 1, 1, 1), 0, 0));
            footpanel.add(sendButton, new GridBagConstraints(1, 0, 1, 1, 10, 10,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(1, 1, 1, 1), 0, 0));

            //中间布局
            msgArea = new JTextPane();
            msgArea.setEditable(false);
            textScrollPane = new JScrollPane();
            textScrollPane.setViewportView(msgArea);
            vertical = new JScrollBar(JScrollBar.VERTICAL);
            vertical.setAutoscrolls(true);
            textScrollPane.setVerticalScrollBar(vertical);
            centerpanel.add(textScrollPane, new GridBagConstraints(0, 0, 1, 1, 100, 100,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));

            //设置顶层布局
            panel.add(headpanel, "North");
            panel.add(footpanel, "South");
            panel.add(centerpanel, "Center");

            //聊天信息输入框的监听回车按钮事件
            msgTestField.addKeyListener(new KeyAdapter() {
                @Override
                public void keyTyped(KeyEvent e) {
                    if (e.getKeyChar() == KeyEvent.VK_ENTER) {
                        if (server == null) {
                            JOptionPane.showMessageDialog(frame, "请先开启聊天室的服务器！", "提示", JOptionPane.WARNING_MESSAGE);
                            return;
                        }
                        String text = msgTestField.getText();
                        if (text != null && !text.equals("")) {
                            String resultMsg = "<msg>"+text+"</msg><id>0</id>";
                            adminPrivateThread.get(otherId).send(formatCodeWithMsg(resultMsg,11));
                            msgTestField.setText("");
                            insertMessage(textScrollPane, msgArea, null, df.format(new Date()) + " 你说：", " "+text, vertical, false);
                        }
                    }
                }
            });
            sendButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    String cmd = e.getActionCommand();
                    if (cmd.equals("发送")) {
                        if (server == null) {
                            JOptionPane.showMessageDialog(frame, "请先开启聊天室的服务器！", "提示", JOptionPane.WARNING_MESSAGE);
                            return;
                        }
                        String text = msgTestField.getText();
                        if (text != null && !text.equals("")) {
                            String resultMsg = "<msg>"+text+"</msg><id>0</id>";
                            adminPrivateThread.get(otherId).send(formatCodeWithMsg(resultMsg,11));
                            msgTestField.setText("");
                            insertMessage(textScrollPane, msgArea, null, df.format(new Date()) + " 你说：", " "+text, vertical, false);
                        }
                    }
                }
            });
            //窗口显示
            setVisible(true);
        }
    }

    /**
     * @MethodName setUIStyle
     * @Params  * @param null
     * @Description 根据操作系统自动变化GUI界面风格
     * @Return
     * @Since 2020/6/6
     */
    public static void setUIStyle() {
//        String lookAndFeel = UIManager.getSystemLookAndFeelClassName(); //设置当前系统风格
        String lookAndFeel = UIManager.getCrossPlatformLookAndFeelClassName(); //可跨系统
        try {
            UIManager.setLookAndFeel(lookAndFeel);
            UIManager.put("Menu.font", new Font("宋体", Font.PLAIN, 12));
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InstantiationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (UnsupportedLookAndFeelException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}


//client

package chatroom;

import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static chatroom.CloseUtils.close;

//聊天室客户端GUI

public class Client {
    private JFrame frame;//构造一个最初不可见的新框架
    //头部参数
    private JTextField host_textfield;//服务器IP
    private JTextField port_textfield;//端口号
    private JTextField name_textfield;//输入姓名
    private JButton head_connect;//连接按钮
    private JButton head_exit;//退出按钮
    //底部参数
    private JTextField text_field;//聊天输入
    private JButton foot_send;//“发送”按钮
    private JButton foot_sysClear;//“清空系统消息”按钮
    private JButton foot_userClear;//“清空聊天消息”按钮

    //右边参数
    private JLabel users_label;//添加一块白板
    private JButton privateChat_button;//“私聊”按钮
    private JButton shield_button;//“屏蔽对方”按钮
    private JButton unshield_button;//“取消屏蔽”按钮
    private JList<String> userlist;//"在线用户列表"的列表
    private DefaultListModel<String> users_model;
    //使用DefaultListModel从JList中添加，删除项目
    private HashMap<String, Integer> users_map;
    //HashMap类使用哈希表来实现Map接口。它允许基本操作：get()和put()
    //在哈希表中进行添加，删除，查找等操作，性能十分之高

    //左边参数
    private JScrollPane sysTextScrollPane;
    //JscrollPane 用于制作组件的可滚动视图。
    //当屏幕尺寸受到限制时，我们使用滚动窗格来显示大型组件或其大小可以动态变化的组件。
    private JTextPane sysMsgArea;
    //系统消息的显示——可以用图形表示的属性标记的文本组件。
    private JScrollBar sysVertical;
    //滚动条的实现。用户将旋钮定位在滚动条中以确定查看区域的内容。

    //中间参数
    private JScrollPane userTextScrollPane;
    private JTextPane userMsgArea;
    //用户消息的显示
    private JScrollBar userVertical;

    //发送和接受参数
    private DataOutputStream dos;
    //数据输出流允许一端以可移植方式将基元数据态 Java 数据类型写入输出流。
    private Receive receive;
    //然后，另一端可以使用数据输入流将数据读回
    private Socket charClient;
    //客户端通信接口

    //时间格式化工具类
    static private SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");//全局变量设置时间

    //当前用户的id
    private int id;

    //私聊窗口Map
    private HashMap<String, privateChatFrame> privateChatFrameMap;//私聊界面的板块

    public static void main(String[] args) {
        new Client().init();
    }//极简主函数启动


    //init——客户端GUI界面初始化，各种监听事件绑定
    public void init() {
        users_map = new HashMap<>();//用户主界面
        privateChatFrameMap = new HashMap<>();//私聊界面
        /*设置窗口的UI风格和字体*/
        setUIStyle();
        frame = new JFrame("Hcode聊天室用户端");//构造一个最初不可见的新框架，可广泛编辑
        JPanel panel = new JPanel();        /*主要的panel，上层放置连接区，下层放置消息区，中间是消息面板，左边是系统消息，右边是当前room的用户列表*/
        JPanel headpanel = new JPanel();    /*上层panel，用于放置连接区域相关的组件*/
        JPanel footpanel = new JPanel();    /*下层panel，用于放置发送信息区域的组件*/
        JPanel centerpanel = new JPanel();    /*中间panel，用于放置聊天信息*/
        JPanel leftpanel = new JPanel();    /*左边panel，用于放置房间列表和加入按钮*/
        JPanel rightpanel = new JPanel();   /*右边panel，用于放置房间内人的列表*/

        /*顶层的布局，分中间，东南西北五个部分*/
        BorderLayout layout = new BorderLayout();

        /*格子布局，主要用来设置西、东、南三个部分的布局*/
        GridBagLayout gridBagLayout = new GridBagLayout();

        /*主要设置北部的布局*/
        FlowLayout flowLayout = new FlowLayout();

        /*设置初始窗口的一些性质*/
        frame.setSize(800, 600);

        frame.setLocationRelativeTo(null);
        //设置窗口相对于指定组件的位置——如果组件当前未显示或者 c 为 null，则此窗口将置于屏幕的中央。

        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        //不执行任何操作
        //要求程序在已注册的WindowListener对象的windowClosing方法中处理该操作，窗口无法关闭。


        frame.setContentPane(panel);
        //对JFrame添加组件
        //建立一个Jpanel或JDesktopPane之类的中间容器，把组件添加到容器中，用setContentPane()方法把该容器置为JFrame的内容面板：
        //　　　　JPanel  contentPane=new  JPanel();
        //把其它组件添加到Jpanel中;
        //　　　　frame.setContentPane(contentPane);
        //把contentPane对象设置成为frame的内容面板

        frame.setLayout(layout);//border
        //Java布局管理器setLayout()——4种——border,flow,grid,card
        //组件在窗体中从左到右依次排列
        //如果排到行的末尾，换行排列
        //排列会随着窗体的大小而改变

        /*设置各个部分的panel的布局和大小*/  //后设置就会在之前全局的border(很好的框架-东西南北中)上分布
        headpanel.setLayout(flowLayout);
        footpanel.setLayout(gridBagLayout);
        leftpanel.setLayout(gridBagLayout);
        centerpanel.setLayout(gridBagLayout);
        rightpanel.setLayout(gridBagLayout);

        //设置面板大小
        leftpanel.setPreferredSize(new Dimension(200, 0));//if 0,take it all
        rightpanel.setPreferredSize(new Dimension(155, 0));
        footpanel.setPreferredSize(new Dimension(0, 40));

        //头部布局
        host_textfield = new JTextField("127.0.0.1");//127.0.0.1是回送地址，指本地机，一般用来测试使用
        port_textfield = new JTextField("8888");//端口号设置
        name_textfield = new JTextField("匿名");//默认设置为匿名
        host_textfield.setPreferredSize(new Dimension(100, 25));
        port_textfield.setPreferredSize(new Dimension(70, 25));
        name_textfield.setPreferredSize(new Dimension(150, 25));

        JLabel host_label = new JLabel("服务器IP:");
        JLabel port_label = new JLabel("端口:");
        JLabel name_label = new JLabel("昵称:");
        //JLabel等是一些基础组件：构造器
        //它必须置于某个容器里，类似于窗花、剪纸，必须置于窗户的表面
        //Flow——Label标记元件配置在显示区的中央部分，钮面文字为text，从左到右

        head_connect = new JButton("连接");
        head_exit = new JButton("退出");

        //真正的顶层组装
        headpanel.add(host_label);
        headpanel.add(host_textfield);
        headpanel.add(port_label);
        headpanel.add(port_textfield);
        headpanel.add(name_label);
        headpanel.add(name_textfield);
        headpanel.add(head_connect);
        headpanel.add(head_exit);

        //底部布局
        foot_send = new JButton("发送");//没有设置大小，默认大小
        foot_sysClear = new JButton("清空系统消息");
        foot_sysClear.setPreferredSize(new Dimension(193, 0));//0应该是全局，但在border限制的范围内
        foot_userClear = new JButton("清空聊天消息");
        foot_userClear.setPreferredSize(new Dimension(148, 0));

        text_field = new JTextField();
        footpanel.add(foot_sysClear, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(1, 1, 1, 1), 0, 0));
        footpanel.add(text_field, new GridBagConstraints(1, 0, 1, 1, 100, 100,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(1, 1, 1, 1), 0, 0));
        footpanel.add(foot_send, new GridBagConstraints(2, 0, 1, 1, 1.0, 1.0,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(1, 1, 1, 1), 0, 0));
        footpanel.add(foot_userClear, new GridBagConstraints(3, 0, 1, 1, 1.0, 1.0,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(1, 1, 1, 1), 0, 0));


        //左边布局
        JLabel sysMsg_label = new JLabel("系统消息：");
        sysMsgArea = new JTextPane();
        sysMsgArea.setEditable(false);//系统不可点击等编辑状态
        sysTextScrollPane = new JScrollPane();
        sysTextScrollPane.setViewportView(sysMsgArea);//相结合：滚动区域和图形表示的文本组件——创建一个视口，并在括号内设置其视图

        sysVertical = new JScrollBar(JScrollBar.VERTICAL);//用于实现滚动条
        sysVertical.setAutoscrolls(true);//动态鼠标点击可以移动

        sysTextScrollPane.setVerticalScrollBar(sysVertical);//相结合：滚动条和滚动区域

        leftpanel.add(sysMsg_label, new GridBagConstraints(0, 0, 1, 1, 1, 1,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
        leftpanel.add(sysTextScrollPane, new GridBagConstraints(0, 1, 1, 1, 100, 100,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));

        //右边布局
        users_model = new DefaultListModel<>();//默认化设置
        //使用DefaultListModel从JList中添加，删除项目——对这个类的内容添加或者减少，那么JList会很快的做出响应
        //他继承自AbstractListModel类，内部用一个Vector来————存储数据
        userlist = new JList<String>(users_model);
        JScrollPane userListPane = new JScrollPane(userlist);
        //创建一个显示指定组件内容的 JScrollPane，只要组件的内容大于视图，就会出现水平和垂直滚动条
        users_label = new JLabel("聊天室内人数：0");
        privateChat_button = new JButton("私聊");
        shield_button = new JButton("屏蔽对方");
        unshield_button = new JButton("取消屏蔽");
        rightpanel.add(users_label, new GridBagConstraints(0, 0, 1, 1, 1, 1,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
        rightpanel.add(privateChat_button, new GridBagConstraints(0, 1, 1, 1, 1, 1,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
        rightpanel.add(shield_button, new GridBagConstraints(0, 2, 1, 1, 1, 1,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
        rightpanel.add(unshield_button, new GridBagConstraints(0, 3, 1, 1, 1, 1,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
        rightpanel.add(userListPane, new GridBagConstraints(0, 4, 1, 1, 100, 100,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));

        //中间布局
        JLabel userMsg_label = new JLabel("世界聊天：");
        userMsgArea = new JTextPane();
        userMsgArea.setEditable(false);
        userTextScrollPane = new JScrollPane();//先实例化
        userTextScrollPane.setViewportView(userMsgArea);//后结合
        userVertical = new JScrollBar(JScrollBar.VERTICAL);
        userVertical.setAutoscrolls(true);
        userTextScrollPane.setVerticalScrollBar(userVertical);

        centerpanel.add(userMsg_label, new GridBagConstraints(0, 0, 1, 1, 1, 1,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
        centerpanel.add(userTextScrollPane, new GridBagConstraints(0, 1, 1, 1, 100, 100,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));

        /*设置顶层布局*/
        panel.add(headpanel, "North");
        panel.add(footpanel, "South");
        panel.add(leftpanel, "West");
        panel.add(rightpanel, "East");
        panel.add(centerpanel, "Center");


        //将按钮事件全部注册到监听器
        allActionListener allActionListener = new allActionListener();//实例化
        //连接服务器
        head_connect.addActionListener(allActionListener);
        //往服务器发送消息setDefaultCloseOperation
        foot_send.addActionListener(allActionListener);
        //退出聊天室
        head_exit.addActionListener(allActionListener);
        //清空系统消息
        foot_sysClear.addActionListener(allActionListener);
        //清空世界聊天消息
        foot_userClear.addActionListener(allActionListener);
        //私聊
        privateChat_button.addActionListener(allActionListener);
        //屏蔽
        shield_button.addActionListener(allActionListener);
        //取消屏蔽
        unshield_button.addActionListener(allActionListener);

        //窗口关闭事件
        frame.addWindowListener(new WindowAdapter() {//无参数构造函数
            //此类是用于创建侦听器对象的便捷类（自由设置）——WindowAdapter类是一个用于接收窗口事件的抽象(适配器)类
            @Override
            public void windowClosing(WindowEvent e) {//类方法——窗口正处于关闭状态时调用
                int option = JOptionPane.showConfirmDialog(frame, "确定关闭聊天室界面?", "提示",
                        JOptionPane.YES_NO_OPTION);//弹窗
                //JOptionPane.showConfirmDialog(parentComponent, message, title, optionType)
                //这种方法是通过询问一个确认问题（例如yes / no / cancel）来获得用户输入的快速简便的方法。

                if (option == JOptionPane.YES_OPTION) {
                    if (e.getWindow() == frame) {//返回事件的发起者,得到 “发起事件的Window对象”
                        if (receive != null) {
                            sendMsg("exit", ""); //如果已连接就告诉服务器本客户端已断开连接，退出聊天室
                        }

                        frame.dispose();
                        //dispose关闭了窗口，释放一部分资源
                        System.exit(0);
                        //系统退出
                    } else {
                        return;
                    }
                } else {
                    return;
                }
            }
        });

        //聊天信息输入框的监听回车按钮事件
        text_field.addKeyListener(new KeyAdapter() {//无参数构造函数
            @Override
            public void keyTyped(KeyEvent e) {
                //此类是用于创建侦听器对象的便捷类——KeyAdapter类是一个用于接收键盘事件的抽象(适配器)类。
                if (e.getKeyChar() == KeyEvent.VK_ENTER) {
                    if (charClient == null || receive == null) {
                        JOptionPane.showMessageDialog(frame, "请先连接服务器进入聊天室！", "提示", JOptionPane.WARNING_MESSAGE);
                        return;
                    }
                    String text = text_field.getText();
                    if (text != null && !text.equals("")) {
                        sendMsg("msg", text);//发送消息
                        text_field.setText("");//发送完消息重置输入框
                        //getText ()方法用于检索数据，String变量来存储数据，setText ()方法在应用别处显示数据
                    }
                }
            }
        });


        //窗口显示
        frame.setVisible(true);

        String name = JOptionPane.showInputDialog("请输入聊天所用昵称：");
        if (name != null &&!name.equals("")) {
            name_textfield.setText(name);
        }
    }//init结束

/*
    public static boolean ipCheckHost(String text) {//一个判断方法——返回true/false
        if (text != null && !text.isEmpty()) {
            // 定义正则表达式
            String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."+
                    "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."+
                    "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."+
                    "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
            // 判断ip地址是否与正则表达式匹配
            if (text.matches(regex)) {
                // 返回判断信息
                return true;
            } else {
                // 返回判断信息
                return false;
            }
        }
        return false;
    }
*/
     //MethodName ipCheck
     //验证ip格式是否正确
    public static boolean ipCheckHost(String text) {
        return text.matches("^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."+
                "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."+
                "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."+
                "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$");
    }

     //MethodName ipCheckPort
     //验证端口格式是否准确
    public static boolean ipCheckPort(String text){
        return text.matches("([0-9]|[1-9]\\d{1,3}|[1-5]\\d{4}|6[0-5]{2}[0-3][0-5])");
    }

     //ClassName allActionListeners
     //全局监听事件，监听所有按钮，输入框，内部类
    private class allActionListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            String cmd = e.getActionCommand();
            //返回的是事件源组件的“命令” ， 这个“命令” 实际上就是事件源组件上的“Label（标签）字符串”
            switch (cmd) {
                case "连接":
                    //获取文本框里面的ip和port
                    String strhost = host_textfield.getText();
                    String strport = port_textfield.getText();
                    if (!ipCheckHost(strhost)){
                        JOptionPane.showMessageDialog(frame, "请检查ip格式是否准确！", "错误", JOptionPane.ERROR_MESSAGE);
                        break;
                    }
                    if(!ipCheckPort(strport)){
                        JOptionPane.showMessageDialog(frame, "请检查端口号是否为0~65535之间的整数！", "错误", JOptionPane.ERROR_MESSAGE);
                        break;
                    }
                    //连接服务器，开启线程
                    connectServer(strhost, Integer.parseInt(strport));
//                    String name = JOptionPane.showInputDialog("请输入你的昵称："); /*提示输入昵称*/
//                    name_textfield.setText(name);

                    /*发送设置姓名的消息和列出用户列表的消息*/
//                    send.setMsg("setName", name);
                    break;
                case "退出":
                    //除了“连接”按钮之外的标配选择，检查初始化
                    if (charClient == null || receive == null) {
                        JOptionPane.showMessageDialog(frame, "对不起，您现在不在聊天室，无法退出！", "错误", JOptionPane.ERROR_MESSAGE);
                        break;
                    }

                    sendMsg("exit", "");
                    head_connect.setText("连接");//回归初始模式
                    head_exit.setText("已退出");//形式上结束通信
                    port_textfield.setEditable(true);//回归初始模式
                    name_textfield.setEditable(true);//回归初始模式
                    host_textfield.setEditable(true);//回归初始模式
                    try {
                        charClient.close();//通信关闭
                    } catch (IOException ioException) {
                        JOptionPane.showMessageDialog(frame, "关闭连接服务器失败，请重启客户端！", "错误", JOptionPane.ERROR_MESSAGE);
                        //退出时出现异常，聊天整体框架失效，需要重新进入
                    }
                    showEscDialog("您已成功退出聊天室！");
                    charClient = null;
                    receive = null;
                    break;
                case "发送":
                    if (charClient == null || receive == null) {
                        JOptionPane.showMessageDialog(frame, "请先连接服务器进入聊天室！", "提示", JOptionPane.WARNING_MESSAGE);
                        break;
                    }

                    String text = text_field.getText();
                    if (text != null && !text.equals("")) {
                        sendMsg("msg", text);
                        //msg 命令是一个命令提示符命令，用于使用命令提示符向网络上的一个或多个用户发送消息
                        //触发命令时，发送到的计算机上会显示一个提示，其中显示消息以及发件人的用户名和发送消息的时间
                        text_field.setText("");
                    }
                    break;
                case "私聊":
                    //服务器未连接
                    if (charClient == null || receive == null) {
                        JOptionPane.showMessageDialog(frame, "请先连接服务器进入聊天室！", "提示", JOptionPane.WARNING_MESSAGE);
                        break;
                    }

                    String selected = userlist.getSelectedValue();//返回最小选定单元格索引的值； 仅在列表中选择单个项目时的选定值
                    //私聊自己
                    if (selected.equals(getUserName(String.valueOf(id)))) {
                        JOptionPane.showMessageDialog(frame, "你不能私聊自己！", "警告", JOptionPane.WARNING_MESSAGE);
                        break;
                    }

                    //已有私聊窗口
                    if (privateChatFrameMap.containsKey(users_map.get(selected).toString())) {
                        JOptionPane.showMessageDialog(frame, "与该用户私聊窗口已存在，请不要重复私聊！", "错误", JOptionPane.ERROR_MESSAGE);
                        break;
                    }

                    if (users_map.containsKey(selected)) {
                        sendMsg("buildPrivateChat", String.valueOf(users_map.get(selected)));
                        //建立沟通弹窗
                    }
                    break;

                case "屏蔽对方":
                    if (charClient == null || receive == null) {
                        JOptionPane.showMessageDialog(frame, "请先连接服务器进入聊天室！", "提示", JOptionPane.WARNING_MESSAGE);
                        break;
                    }

                    String selectedShield = userlist.getSelectedValue();
                    //如果是自己
                    if (selectedShield.equals(getUserName(String.valueOf(id)))) {
                        JOptionPane.showMessageDialog(frame, "你不能屏蔽自己！", "警告", JOptionPane.WARNING_MESSAGE);
                        break;
                    }

                    //不准屏蔽管理员！
                    if (selectedShield.equals(getUserName(String.valueOf(0)))) {//管理员是第一个上线的
                        JOptionPane.showMessageDialog(frame, "对不起，不支持屏蔽管理员！", "警告", JOptionPane.WARNING_MESSAGE);
                        break;
                    }

                    //不能重复屏蔽！
                    if (selectedShield.indexOf("(已屏蔽)") != -1) {
                        JOptionPane.showMessageDialog(frame, "对方已被屏蔽了！请不要重复操作！", "错误", JOptionPane.ERROR_MESSAGE);
                        break;
                    }

                    if (users_map.containsKey(selectedShield)) { //发送需要屏蔽用户的id
                        sendMsg("shield", String.valueOf(users_map.get(selectedShield)));
                        users_map.put(selectedShield + "(已屏蔽)", users_map.get(selectedShield));
                        //hashmap.put（K key，V value）
                        //如果插入的 key 对应的 value 已经存在，则执行 value 替换操作
                        //如果不存在则执行插入，返回 null
                        users_map.remove(selectedShield);//屏蔽，在列表中移除原先的标签
                    }

                    //重组标签——默认+自拟
                    int index1 = users_model.indexOf(selectedShield);
                    users_model.set(index1, selectedShield + "(已屏蔽)");
                    break;

                case "取消屏蔽":
                    if (charClient == null || receive == null) {
                        JOptionPane.showMessageDialog(frame, "请先连接服务器进入聊天室！", "提示", JOptionPane.WARNING_MESSAGE);
                        break;
                    }

                    String unShield = userlist.getSelectedValue();
                    if (unShield.indexOf("(已屏蔽)") == -1) {
                        JOptionPane.showMessageDialog(frame, "对方并未被屏蔽！", "警告", JOptionPane.WARNING_MESSAGE);
                        break;
                    }//没有被屏蔽，也没有必要去解除屏蔽

                    String name = unShield.substring(0, unShield.indexOf("(已屏蔽)"));
                    //substring() 方法返回字符串的子字符串
                    //public String substring(int beginIndex)
                    //或
                    //public String substring(int beginIndex, int endIndex)
                    sendMsg("unshield", String.valueOf(users_map.get(unShield)));

                    int index2 = users_model.indexOf(unShield);
                    users_model.set(index2, name);//静态的图片随于List；标号和姓名set关联在一起

                    users_map.put(name, users_map.get(unShield));//HashMap动态可以处理的模式
                    users_map.remove(unShield);
                    break;

                case "清空系统消息":
                    sysMsgArea.setText("");//空字符串
                    break;
                case "清空聊天消息":
                    userMsgArea.setText("");//空字符串
                    break;
                default:
                    break;
            }

        }
    }

     //MethodName connectServer
     //开启与服务器的连接——开启接受服务器的指令 与 信息的Receive线程类
    private boolean connectServer(String host, int port) {

        try {
            charClient = new Socket(host, port);
            dos = new DataOutputStream(charClient.getOutputStream());
            //创建新的数据输出流以将数据写入指定的基础输出流。 写入的计数器设置为零。
            //out - 底层输出流，保存以供以后使用

            receive = new Receive(charClient, this);

            (new Thread(receive)).start();//接受服务器的消息的线程(系统消息和其他网友的信息）
            head_connect.setText("已连接");
            head_exit.setText("退出");
            port_textfield.setEditable(false);
            name_textfield.setEditable(false);
            host_textfield.setEditable(false);
            sendMsg("new", name_textfield.getText()); //后续写在登录窗口
            sendMsg("getList", "");
            return true;
        } catch (IOException e) {
            JOptionPane.showMessageDialog(frame, "连接服务器失败！", "错误", JOptionPane.ERROR_MESSAGE);
            return false;
        }
    }

     //MethodName sendMsg
     //发送指令与信息给服务器端对应的线程服务
    private void sendMsg(String cmd, String msg) {
        try {
            dos.writeUTF("<cmd>" + cmd + "</cmd><msg>" + msg + "</msg>");//一连串的字符串通过“+“连接
            //java.io.DataOutputStream 类的 writeUTF（） 方法接受 String 值作为参数，并使用修改后的 UTF-8 编码将其写入当前输出流
            //java.io.DataOutputStream 的 readUTF（） 方法将采用修改后的 UTF-8 编码的数据读取到字符串中并返回
            dos.flush();
            //刷新此数据输出流。 这会强制将任何缓冲的输出字节写出到流中。
        } catch (IOException e) {
            close(dos, charClient);//异常情况：相当于空——无效指令
        }
    }

    public void setId(int id) {
        this.id = id;
    }

     //MethodName updateTextArea
     //更新 系统文本域 或 聊天事件文本域
    public void updateTextArea(String content, String where) {
        if (content.length() > 0) {
            Matcher matcher = null;
            //Pattern 对象是正则表达式编译后在内存中的表示形式，因此，正则表达式字符串必须先被编译为 Pattern 对象，
            //然后再利用该 Pattern 对象创建对应的 Matcher 对象。执行匹配所涉及的状态保留在 Matcher 对象中，多个 Matcher 对象可共享同一个 Pattern 对象
            if (where.equals("user")) {

                //正则表达式(regular expression)描述了一种字符串匹配的模式（pattern）
                //可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。
                Pattern pattern = Pattern.compile("<userId>(.*)</userId><userMsg>(.*)</userMsg><time>(.*)</time>");
                //用圆括号 () 将所有选择项括起来，相邻的选择项之间用 | 分隔
                //() 表示捕获分组，() 会把每个分组里的匹配的值保存起来， 多个匹配值可以通过数字 n 来查看(n 是一个数字，表示第 n 个捕获组的内容)。

                matcher = pattern.matcher(content);

                if (matcher.find()) {//find()返回目标字符串中是否包含与 Pattern 匹配的子串
                    String userId = matcher.group(1);
                    //group()返回上一次与 Pattern 匹配的子串
                    //group是针对（）来说的，group（0）就是指的整个串，group（1） 指的是第一个括号里的东西，group（2）指的第二个括号里的东西
                    String userMsg = matcher.group(2);
                    String time = matcher.group(3);
//                if(userMsg.startsWith("<emoji>")){
//                    String emojiCode = userMsg.substring(7, smsg.length()-8);
//                    insertMessage(userTextScrollPane, userMsgArea, emojiCode, fromName+"说：", null,userVertical);
//                    return ;
//                }
                    if (userId.equals("0")) {
                        insertMessage(userTextScrollPane, userMsgArea, null, getUserName(userId) + " "+time , " "+userMsg, userVertical, false);
                    } else {
                        String fromName = getUserName(userId);
                        if (fromName.equals("[用户" + id + "]" + name_textfield.getText())) //如果是自己说的话
                            fromName = "你";
                        insertMessage(userTextScrollPane, userMsgArea, null,  fromName + " "+time, " "+userMsg, userVertical, false);
                    }
                }
            } else {
                Pattern pattern = Pattern.compile("<time>(.*)</time><sysMsg>(.*)</sysMsg>");
                matcher = pattern.matcher(content);
                if (matcher.find()) {
                    String sysTime = matcher.group(1);//time
                    String sysMsg = matcher.group(2);//系统消息
                    insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统消息] " + sysTime, sysMsg, sysVertical, true);
                }
            }
        }
    }

     //MethodName insertMessage
     //更新 文本域信息 格式化工具——多个用户协同在看
    private void insertMessage(JScrollPane scrollPane, JTextPane textPane, String icon_code,
                               String title, String content, JScrollBar vertical, boolean isSys) {

        StyledDocument document = textPane.getStyledDocument();     /*获取textpane中的文本*/
        //通用样式文档的接口，Document是用于文本的容器，用作swing文本组件的模型
        //获取与编辑器关联的模型

        /*设置标题的属性*/
        Color content_color = null;//初始化

        //Color 类用于封装默认 sRGB 颜色空间中的颜色或由 ColorSpace 标识的任意颜色空间中的颜色
        if (isSys) {
            content_color = Color.RED;
        } else {
            content_color = Color.GRAY;
        }

        //SimpleAttributeSet是一个样式属性集合。包含了字体大小、下划线、对齐等属性。
        //通过StyleConstants类方法进行设置。
        //设置好属性后，可以通过特定对象将样式应用到文本。
        SimpleAttributeSet title_attr = new SimpleAttributeSet();
        StyleConstants.setBold(title_attr, true);
        //setBold(MutableAttributeSet a, boolean b)	设置粗体
        StyleConstants.setForeground(title_attr, Color.BLUE);
        //setForeground(attr, fg)字体颜色

        /*设置正文的属性*/
        SimpleAttributeSet content_attr = new SimpleAttributeSet();
        StyleConstants.setBold(content_attr, false);//不加粗
        StyleConstants.setForeground(content_attr, content_color);

        Style style = null;
        //与文档中的元素相关联的属性集合。
        //由于这些通常用于将 字符和段落 样式与 元素 相关联，因此提供了用于此的操作。

        if (icon_code != null) {//正常应该为空，但是图片则不然(emoji)
            Icon icon = new ImageIcon("icon/" + icon_code + ".png");
            //固定尺寸的小图片，通常用于装饰组件

            style = document.addStyle("icon", null);//复制出名字，放到图片上
            //将新样式添加到逻辑样式层次结构中。nm – 样式的名称（在命名样式集合中必须是唯一的）。
            //样式属性自下而上解析，因此子项中指定的属性将覆盖父项中指定的属性。

            StyleConstants.setIcon(style, icon);//设置图标属性——文字+图标————成功装配聊天消息
        }

        /*聊天消息的呈现*/
        try {
            document.insertString(document.getLength(), title + "\n", title_attr);
            //插入内容字符串
            //offset – 文档中插入内容的偏移量 >= 0。在给定位置或之后跟踪更改的所有位置都将移动。//getLength()返回文档中当前内容的字符数
            //每当打开一个文件都有一个"当前文件偏移量"(current file offset)
                //它通常是非负整数,用以度量从文件开始处计算的字节数.通常,读,写操作都是从当前文件偏移量开始,并使偏移量增加所读写的字节数
            //str - 要插入的字符串
            //a – 与插入内容相关联的属性。 如果没有属性，这可能为 null

            if (style != null)
                document.insertString(document.getLength(), "\n", style);//图片+换行——图片格式
            else
                document.insertString(document.getLength(), content + "\n", content_attr);//文本+换行——正文格式

        } catch (BadLocationException ex) {
            System.out.println("Bad location exception");
        }


        /*设置滑动条到最后*/
        textPane.setCaretPosition(textPane.getDocument().getLength());//实时更新变化
        //设置 TextComponent 的文本插入符号的位置。 该位置必须介于 0 和组件文本的长度之间，否则将引发异常。
//        vertical.setValue(vertical.getMaximum());
    }


     //MethodName getUserName
     //在users_map中根据 value值用户ID 获取key值 的用户名字
    private String getUserName(String strId) {//标题栏目里面的name

        int uid = Integer.parseInt(strId);//parseInt(s)用于解析整数(包含整数即可——相当于提取)
        //s - 一个包含要解析的 int 表示的字符串
        //return——由十进制参数表示的整数值。

        Set<String> set = users_map.keySet();
        //不包含重复元素的集合
        //此地图中包含的键的集合视图

        Iterator<String> iterator = set.iterator();
        //集合上的迭代器。 迭代器在 Java 集合框架中取代了枚举。

        String cur = null;
        while (iterator.hasNext()) {//boolean
            //如果迭代有更多元素，则返回 true。
            //换句话说，如果 next 将返回一个元素而不是抛出异常，则返回 true。
            cur = iterator.next();
            if (users_map.get(cur) == uid) {
                return cur;
            }
        }
        return "";
    }



     //MethodName showEscDialog
     //处理当前客户端用户断开与服务器连接的一切事务
    public void showEscDialog(String content) {

        //清除所有私聊
        if (privateChatFrameMap.size() != 0) {

            Set<Map.Entry<String, privateChatFrame>> entrySet = privateChatFrameMap.entrySet();
            //Map.entrySet() 这个方法返回的是一个Set<Map.Entry<K,V>>
            //Map.Entry 是Map中的一个接口，他的用途是表示一个映射项（里面有Key和Value），而Set<Map.Entry<K,V>>表示一个映射项的Set

            Iterator<Map.Entry<String, privateChatFrame>> iter = entrySet.iterator();

            while (iter.hasNext())
            {
                Map.Entry<String, privateChatFrame> entry = iter.next();
                //Map.Entry里有相应的getKey和getValue方法，即JavaBean，让我们能够从一个项中取出Key和Value
                entry.getValue().dispose(); //关闭对应窗口 (此条目对应的值)
                sendMsg("privateExit", entry.getKey()); //想对方说明私聊结束
            }
        }

        //关闭输出流

        close(dos, charClient);
        receive.setRunning(false);

        //输入框可编辑
        port_textfield.setEditable(true);
        name_textfield.setEditable(true);
        host_textfield.setEditable(true);
        head_connect.setText("连接");
        head_exit.setText("已退出");
        insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统消息] " + df.format(new Date()), content, sysVertical, true);
        JOptionPane.showMessageDialog(frame, content, "提示", JOptionPane.WARNING_MESSAGE);

        /*清除消息区内容，清除用户数据模型内容和用户map内容，更新房间内人数*/
        userMsgArea.setText("");
//        sysMsgArea.setText("");
        users_map.clear();
        users_model.removeAllElements();
        users_label.setText("聊天室内人数：0");//对于单个来说一切归零
    }



     //MethodName addUser
     //当有新的用户加入聊天室，系统文本域的更新和用户列表的更新
    public void addUser(String content) {
        if (content.length() > 0) {
            Pattern pattern = Pattern.compile("<username>(.*)</username><id>(.*)</id>");
            Matcher matcher = pattern.matcher(content);
            if (matcher.find()) {
                String name = matcher.group(1);
                String id = matcher.group(2);
                if (!users_map.containsKey(name)) {
                    users_map.put("[用户" + id + "]" + name, Integer.parseInt(id));
                    users_model.addElement("[用户" + id + "]" + name);
                } else {
                    users_map.remove("[用户" + id + "]" + name);
                    users_model.removeElement(name);
                    users_map.put("[用户" + id + "]" + name, Integer.parseInt(id));
                    users_model.addElement("[用户" + id + "]" + name);
                }
                insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统消息] " + df.format(new Date()), "[用户" + id + "]" + name + " 加入了聊天室", sysVertical, true);
            }
        }
        users_label.setText("聊天室内人数：" + users_map.size()); //更新房间内的人数
    }



     //MethodName delUser
     //当有用户退出时，系统文本域的通知和用户列表的更新

    public void delUser(String content) {
        if (content.length() > 0) {
            //Set<String> set = users_map.keySet();
            String delName = getUserName(content);
            insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统消息] " + df.format(new Date()), delName + " 退出了聊天室", sysVertical, true);
            users_map.remove(delName);
            users_model.removeElement(delName);
        }
        users_label.setText("聊天室内人数：" + users_map.size());//更新房间内的人数
    }



     //MethodName updateUsername
     //修改指定ID用户的昵称（暂时用不到）

    public void updateUsername(String content) {
        if (content.length() > 0) {
            Pattern pattern = Pattern.compile("<id>(.*)</id><username>(.*)</username>");
            Matcher matcher = pattern.matcher(content);
            if (matcher.find()) {
                String id = matcher.group(1);
                String name = matcher.group(2);
                if (id.equals("0")) {
                    users_map.remove("[管理员]" + name);
                    users_model.removeElementAt(0);
                    users_map.put("[管理员]" + name, Integer.parseInt(id));
                    users_model.addElement("[管理员]" + name);
                } else if (users_map.get("[用户" + id + "]" + name) != Integer.parseInt(id)) {
                    users_map.put("[用户" + id + "]" + name, Integer.parseInt(id));
                    users_model.addElement("[用户" + id + "]" + name);
                } else {
                    users_map.remove("[用户" + id + "]" + name);
                    users_model.removeElement("[用户" + id + "]" + name);
                    users_map.put("[用户" + id + "]" + name, Integer.parseInt(id));
                    users_model.addElement("[用户" + id + "]" + name);
                }
            }
        }
    }



     //MethodName getUserList
     //从服务器获取全部用户信息的列表，解析信息格式，列出所有用户
    public void getUserList(String content) {
        String name = null;
        String id = null;
        Pattern numPattern = null;
        Matcher numMatcher = null;
        Pattern userListPattern = null;

        if (content.length() > 0) {
            numPattern = Pattern.compile("<user>(.*?)</user>");
            numMatcher = numPattern.matcher(content);
            //遍历字符串，进行正则匹配，获取所有用户信息
            while (numMatcher.find()) {//尝试查找与模式匹配的输入序列的下一个子序列。
                //当且仅当输入序列的子序列与此匹配器的模式匹配时才为真
                String detail = numMatcher.group(1);

                userListPattern = Pattern.compile("<id>(.*)</id><username>(.*)</username>");
                Matcher userListmatcher = userListPattern.matcher(detail);

                if (userListmatcher.find()) {
                    id = userListmatcher.group(1);
                    name = userListmatcher.group(2);
                    if (id.equals("0")) {
                        name = "[管理员]" + name;
                        users_map.put(name, Integer.parseInt(id));
                    } else {
                        name = "[用户" + id + "]" + name;
                        users_map.put(name, Integer.parseInt(id));
                    }
                    users_model.addElement(name);
                }
            }
            users_model.removeElementAt(0);
            //删除指定索引处的组件。
            //index - 要删除的对象的索引
        }
        users_label.setText("聊天室内人数：" + users_map.size());
    }



     //MethodName askBuildPrivateChat
     //处理某用户对当前客户端的用户的私聊请求
    public void askBuildPrivateChat(String msg) {
        Pattern pattern = Pattern.compile("<from>(.*)</from><id>(.*)</id>");
        Matcher matcher = pattern.matcher(msg);
        //Pattern 类：
        //pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。
        //要创建一个 Pattern 对象，你必须首先调用其公共静态编译方法，它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。
        //Matcher 类：
        //Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样，Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。
        if (matcher.find()) {  //当且仅当输入序列的子序列与此匹配器的模式匹配时才为真
            // find()是部分匹配，从当前位置开始匹配，找到一个匹配的子串，将移动下次匹配的位置。
            String toPrivateChatName = matcher.group(1);
            String toPrivateChatId = matcher.group(2);

            int option = JOptionPane.showConfirmDialog(frame, "[" + toPrivateChatName + "]想与你私聊，是否同意？", "提示",
                    JOptionPane.YES_NO_OPTION);

            if (option == JOptionPane.YES_OPTION) {
                sendMsg("agreePrivateChar", toPrivateChatId);
                privateChatFrame chatFrame = new privateChatFrame("与[" + toPrivateChatName + "]的私聊窗口", toPrivateChatName, toPrivateChatId);
                privateChatFrameMap.put(toPrivateChatId, chatFrame);
            } else {
                sendMsg("refusePrivateChar", toPrivateChatId);
            }
        }
    }



     //MethodName startOrStopHisPrivateChat
     //  获取请求指定用户的私聊请求的结果，  同意就开启私聊窗口，拒绝就提示。
    public void startOrStopHisPrivateChat(String msg) {

        Pattern pattern = Pattern.compile("<result>(.*)</result><from>(.*)</from><id>(.*)</id>");
        Matcher matcher = pattern.matcher(msg);
        //       /<.*>/
        //() 表示捕获分组，() 会把每个分组里的匹配的值保存起来， 多个匹配值可以通过数字 n 来查看(n 是一个数字，表示第 n 个捕获组的内容)。
        //下面的表达式匹配从开始小于符号 (<) 到关闭 h1 标记的大于符号 (>) 之间的所有内容——若是一组则有标签注明的作用

        if (matcher.find()) {
            String result = matcher.group(1);//
            String toPrivateChatName = matcher.group(2);
            String toPrivateChatId = matcher.group(3);

            if (result.equals("1")) {  //对方同意的话
                if (toPrivateChatId.equals("0")) {
                    toPrivateChatName = "[管理员]" + toPrivateChatName;//重新赋值
                }
                privateChatFrame chatFrame = new privateChatFrame("与[" + toPrivateChatName + "]的私聊窗口", toPrivateChatName, toPrivateChatId);

                privateChatFrameMap.put(toPrivateChatId, chatFrame);

            } else if (result.equals("0")) {
                JOptionPane.showMessageDialog(frame, "[" + toPrivateChatName + "]拒绝了你的私聊请求", "失败", JOptionPane.ERROR_MESSAGE);
            }
        }
    }



     //MethodName giveMsgToPrivateChat
     //根据服务器端发来的用户ID和内容，搜寻当前客户端的用户中对应传来的用户ID的私聊窗口，将内容写进私聊窗口的文本域
    public void giveMsgToPrivateChat(String msg) {
        Pattern privatePattern = Pattern.compile("<msg>(.*)</msg><id>(.*)</id>");
        Matcher privateMatcher = privatePattern.matcher(msg);
        if (privateMatcher.find()) {
            String toPrivateMsg = privateMatcher.group(1);
            String toPrivateId = privateMatcher.group(2);
            privateChatFrame chatFrame = privateChatFrameMap.get(toPrivateId);
            insertMessage(chatFrame.textScrollPane, chatFrame.msgArea, null, df.format(new Date()) + " 对方说：", " "+toPrivateMsg, chatFrame.vertical, false);
        }
    }

     //MethodName endPrivateChat
     //结束指定id用户的私聊窗口
    public void endPrivateChat(String msg) {
        Pattern privatePattern = Pattern.compile("<id>(.*)</id>");
        Matcher privateMatcher = privatePattern.matcher(msg);

        if (privateMatcher.find()) {
            String endPrivateId = privateMatcher.group(1);
            privateChatFrame chatFrame = privateChatFrameMap.get(endPrivateId);
            //get()——Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key.
            JOptionPane.showMessageDialog(frame, "由于对方结束了私聊，该私聊窗口即将关闭！", "提示", JOptionPane.WARNING_MESSAGE);
            chatFrame.dispose();
            insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统消息] " + df.format(new Date()), "由于[" + chatFrame.otherName + "]关闭了私聊窗口，私聊结束！", sysVertical, true);
            privateChatFrameMap.remove(endPrivateId);
            //Removes the mapping for the specified key from this map if present.
            //key – key whose mapping is to be removed from the map
        }
    }



     //ClassName privateChatFrame
     //私聊窗口GUI的内部类

    private class privateChatFrame extends JFrame {
        private String otherName;
        private String otherId;
        private JButton sendButton;
        private JTextField msgTestField;
        private JTextPane msgArea;
        private JScrollPane textScrollPane;
        private JScrollBar vertical;

        public privateChatFrame(String title, String otherName, String otherId) throws HeadlessException {
            super(title);
            //创建一个具有指定标题的新的、最初不可见的 Frame。
            this.otherName = otherName;
            this.otherId = otherId;
            //全局面板容器
            JPanel panel = new JPanel();
            //全局布局
            BorderLayout layout = new BorderLayout();

            JPanel headpanel = new JPanel();    //上层panel，
            JPanel footpanel = new JPanel();    //下层panel
            JPanel centerpanel = new JPanel(); //中间panel

            //头部布局
            FlowLayout flowLayout = new FlowLayout();
            //底部布局
            GridBagLayout gridBagLayout = new GridBagLayout();

            setSize(600, 500);
            setLocationRelativeTo(null);
            setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
            setContentPane(panel);
            setLayout(layout);

            headpanel.setLayout(flowLayout);
            footpanel.setLayout(gridBagLayout);
            footpanel.setPreferredSize(new Dimension(0, 40));
            centerpanel.setLayout(gridBagLayout);

            //添加头部部件
            JLabel Name = new JLabel(otherName);
            headpanel.add(Name);

            //设置底部布局
            sendButton = new JButton("发送");
            sendButton.setPreferredSize(new Dimension(40, 0));
            //设置此组件的首选大小。 如果preferredSize 为null，UI 将被要求提供首选尺寸。

            msgTestField = new JTextField();
            footpanel.add(msgTestField, new GridBagConstraints(0, 0, 1, 1, 100, 100,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(1, 1, 1, 1), 0, 0));
            footpanel.add(sendButton, new GridBagConstraints(1, 0, 1, 1, 10, 10,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(1, 1, 1, 1), 0, 0));

            //中间布局
            msgArea = new JTextPane();
            msgArea.setEditable(false);
            textScrollPane = new JScrollPane();
            textScrollPane.setViewportView(msgArea);
            vertical = new JScrollBar(JScrollBar.VERTICAL);
            vertical.setAutoscrolls(true);
            textScrollPane.setVerticalScrollBar(vertical);
            centerpanel.add(textScrollPane, new GridBagConstraints(0, 0, 1, 1, 100, 100,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));

            //设置顶层布局
            panel.add(headpanel, "North");
            panel.add(footpanel, "South");
            panel.add(centerpanel, "Center");

            //窗口关闭事件
            addWindowListener(new WindowAdapter() {
                @Override
                public void windowClosing(WindowEvent e) {
                    int option = JOptionPane.showConfirmDialog(e.getOppositeWindow(), "确定结束私聊？", "提示",
                            JOptionPane.YES_NO_OPTION);
                    if (option == JOptionPane.YES_OPTION) {
                        if (receive != null) {
                            sendMsg("privateExit", otherId); //关闭当前私聊连接
                        }
                        insertMessage(sysTextScrollPane, sysMsgArea, null, "[系统消息] " + df.format(new Date()), "您与[" + otherName + "]的私聊结束", sysVertical, true);
                        dispose();
                        privateChatFrameMap.remove(otherId);
                    } else {
                        return;
                    }
                }
            });

            //聊天信息输入框的监听回车按钮事件
            msgTestField.addKeyListener(new KeyAdapter() {
                @Override
                public void keyTyped(KeyEvent e) {
                    if (e.getKeyChar() == KeyEvent.VK_ENTER) {
                        if (receive == null) {
                            JOptionPane.showMessageDialog(frame, "请先连接聊天室的服务器！", "提示", JOptionPane.WARNING_MESSAGE);
                            return;
                        }

                        String text = msgTestField.getText();
                        if (text != null && !text.equals("")) {
                            sendMsg("privateMsg", "<msg>" + text + "</msg><id>" + otherId + "</id>");
                            msgTestField.setText("");
                            insertMessage(textScrollPane, msgArea, null, df.format(new Date()) + " 你说：", " "+text, vertical, false);
                        }
                    }
                }
            });
            sendButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    String cmd = e.getActionCommand();//return:标识此事件的命令的字符串
                    if (cmd.equals("发送")) {
                        if (receive == null) {
                            JOptionPane.showMessageDialog(frame, "请先连接聊天室的服务器！", "提示", JOptionPane.WARNING_MESSAGE);
                            return;
                        }

                        String text = msgTestField.getText();
                        if (text != null && !text.equals("")) {
                            sendMsg("privateMsg", "<msg>" + text + "</msg><id>" + otherId + "</id>");
                            msgTestField.setText("");
                            insertMessage(textScrollPane, msgArea, null, df.format(new Date()) + " 你说：", " "+text, vertical, false);
                        }
                    }
                }
            });
            //窗口显示
            setVisible(true);
        }
    }



     //MethodName setUIStyle
     //根据操作系统自动变化GUI界面风格

    public static void setUIStyle() {
//       String lookAndFeel = UIManager.getSystemLookAndFeelClassName(); //设置当前系统风格
        String lookAndFeel = UIManager.getCrossPlatformLookAndFeelClassName(); //可跨系统
        //返回实现默认跨平台外观的 LookAndFeel 类的名称
        try {
            UIManager.setLookAndFeel(lookAndFeel);
            //使用当前线程的上下文类加载器加载由给定类名指定的 LookAndFeel，并将其传递给 setLookAndFeel(LookAndFeel)。
            //参形：className – 一个字符串，指定实现外观的类的名称
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InstantiationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (UnsupportedLookAndFeelException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

//Receive

package chatroom;

import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 使用多线程封装:接收端
 * 1、接收消息
 * 2、释放资源
 * 3、重写run
 *
 * @author Himit_ZH
 */
public class Receive implements Runnable {
    private DataInputStream dis;
    private Socket connect;
    private boolean isRunning;
    private Client client;

    public Receive(Socket connect, Client client) {
        this.connect = connect;
        this.isRunning = true;
        this.client = client;
        try {
            dis = new DataInputStream(connect.getInputStream());
        } catch (IOException e) {
            System.out.println("数据输入流初始化错误，请重启");
            release();
        }
    }

    //接收消息
    private String receive() {
        String msg = "";
        try {
            msg = dis.readUTF();
        } catch (IOException e) {
            release();
        }
        return msg;
    }

    public void setRunning(boolean running) {
        isRunning = running;
    }

    @Override
    public void run() {
        while (isRunning) {
            String msg = receive();
            if(!msg.equals("")&&msg!=null) {
                parseMessage(msg); //收到指令与消息的字符串，进行指令分配
            }
        }
    }


    /**
     * @MethodName parseMessage
     * @Params  * @param null
     * @Description 处理服务器发来的指令，将对应信息给予客户端线程相应的方法处理
     * @Return
     * @Since 2020/6/6
     */
    public void parseMessage(String message) {
        String code = null;
        String msg = null;
        /*
         * 先用正则表达式匹配响应code码和msg内容
         */
        if (message.length() > 0) {
            Pattern pattern = Pattern.compile("<cmd>(.*)</cmd>");
            Matcher matcher = pattern.matcher(message);
            if (matcher.find()) {
                code = matcher.group(1);
            }
            pattern = Pattern.compile("<msg>(.*)</msg>");
            matcher = pattern.matcher(message);
            if (matcher.find()) {
                msg = matcher.group(1);
            }
            switch (code) {
                case "1":   //更新世界聊天
                    client.updateTextArea(msg, "user");
                    break;
                case "2":   //更新系统消息
                    client.updateTextArea(msg, "sys");
                    break;
                case "3":   //与服务器断开消息 ：可能为被踢，或者服务器关闭
                    client.showEscDialog(msg);
                    break;
                case "4":  //新增用户
                    client.addUser(msg);
                    break;
                case "5":  //删除用户 有其它用户退出 或者自己退出
                    client.delUser(msg);
                    break;
                case "6":  //更新某个用户更新的名字
                    client.updateUsername(msg);
                    break;
                case "7":  /*列出用户列表*/
                    client.getUserList(msg);
                    break;
                case "8": //设置用户id
                    client.setId(Integer.parseInt(msg));
                    break;
                case "9": //他人私聊于你，请求是否同意
                    client.askBuildPrivateChat(msg);
                    break;
                case "10": //你请求想与他人私聊的结果
                    client.startOrStopHisPrivateChat(msg);
                    break;
                case "11": //对方给你的私聊消息
                    client.giveMsgToPrivateChat(msg);
                    break;
                case "12": //对方结束了私聊
                    client.endPrivateChat(msg);
            }
        }
    }


    //释放资源
    private void release() {
        this.isRunning = false;
        CloseUtils.close(dis, connect);
    }

}

//User

package chatroom;

import java.net.Socket;
public class User {
    private String username;//用户姓名
    private Integer id;//用户id
    private Socket socket;//用户通信

    public User(String username, Integer id, Socket socket) {
        this.username = username;
        this.id = id;
        this.socket = socket;
    }


    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Socket getSocket() {
        return socket;
    }

    public void setSocket(Socket socket) {
        this.socket = socket;
    }
}

//CloseUtils

package chatroom;

import java.io.Closeable;
 //名字：工具类
 //作用：关闭各种资源
public class CloseUtils {
    /**
     * 释放资源
     */
    public static void close(Closeable... targets ) {
        for(Closeable target:targets) {
            try {
                if(null!=target) {
                    target.close();
                }
            }catch(Exception e) {
                e.printStackTrace();
            }
        }
    }
}